package ru.batrdmi.svnplugin;

import com.intellij.ide.DataManager;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.FrameWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.annotate.ShowAllAffectedGenericAction;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.history.CurrentRevision;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vcs.history.VcsHistoryUtil;
import com.intellij.openapi.vcs.vfs.ContentRevisionVirtualFile;
import com.intellij.openapi.vcs.vfs.VcsFileSystem;
import com.intellij.openapi.vcs.vfs.VcsVirtualFile;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.Gray;
import com.intellij.ui.components.JBScrollPane;
import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.model.mxCell;
import com.mxgraph.shape.mxBasicShape;
import com.mxgraph.shape.mxConnectorShape;
import com.mxgraph.swing.handler.mxSelectionCellsHandler;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.*;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.SvnRevisionNumber;
import org.jetbrains.idea.svn.SvnVcs;
import org.jetbrains.idea.svn.history.SvnFileRevision;
import org.jetbrains.idea.svn.history.SvnRepositoryContentRevision;
import org.tmatesoft.svn.core.wc.SVNRevision;
import ru.batrdmi.svnplugin.actions.*;
import ru.batrdmi.svnplugin.gui.ScrollOverlayFactory;
import ru.batrdmi.svnplugin.logic.*;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class SVNRevisionGraph extends FrameWrapper implements mxEventSource.mxIEventListener {
    private static final Logger log = Logger.getInstance("ru.batrdmi.svnplugin.SVNRevisionGraph");
    public static final int CELL_SPACING = 16;
    public static final String IN_BRANCH_LINK_STYLE = "inBranchLinkStyle";
    public static final String COPY_OR_MERGE_LINK_STYLE = "copyOrMergeLinkStyle";
    public static final String REVISION_STYLE = "revisionStyle";
    private static final String REVISION_SHAPE = "revisionShape";
    private static final String LINK_SHAPE = "linkShape";

    private static final Color REVISION_COLOR = new Color(185, 230, 184);
    private static final Color HIGHLIGHTED_REVISION_COLOR = new Color(255, 128, 128);
    private static final Color PARTIALLY_HIGHLIGHTED_REVISION_COLOR = new Color(220, 179, 156);
    private static final Color SELECTED_REVISION_COLOR = new Color(53, 128, 51);
    private static final Color BRANCH_NAMES_COLOR = Gray._110;
    private static final Color BRANCH_SEPARATING_LINE_COLOR = new Color(185, 185, 255);
    private static final Stroke BRANCH_SEPARATING_LINE_STROKE = new BasicStroke(1,
            BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] {1, 2}, 0);
    private static final Color BRANCH_LABEL_COLOR = new Color(255, 255, 255, 215);
    private static final Color BRANCH_LABEL_HIGHLIGHTED_COLOR = new Color(225, 225, 255, 215);
    private static final Color BRANCH_LABEL_BORDER_COLOR = new Color(225, 225, 255);
    private static final Color BRANCH_LABEL_BORDER_HIGHLIGHTED_COLOR = new Color(185, 185, 255);

    private static final int TEXTFIELD_WIDTH_IN_CHARS = 17;

    private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd  HH:mm:ss");

    public static final DataKey<Revision> CURRENT_REVISION = DataKey.create("SVNRevisionGraph.currentRevision");
    public static final DataKey<Revision> SELECTED_REVISION = DataKey.create("SVNRevisionGraph.selectedRevision");
    public static final DataKey<List<Revision>> SELECTED_REVISIONS = DataKey.create("SVNRevisionGraph.selectedRevisions");
    public static final DataKey<FilePath> SRC_FILE = DataKey.create("SVNRevisionGraph.srcFile");
    public static final String REVISION_NUMBER_FIELD = "single";
    public static final String REVISION_NUMBER_COMBOBOX = "multiple";

    static {
        mxGraphics2DCanvas.putShape(REVISION_SHAPE, new RevisionShape());
        mxGraphics2DCanvas.putShape(LINK_SHAPE, new LinkShape());
    }

    public static void createAndShow(Project project, FilePath filePath) {
        SVNRevisionGraphConfiguration configuration = SVNRevisionGraphConfiguration.getInstance(project);
        SVNRevisionGraph graph = new SVNRevisionGraph(project, filePath);
        createOrUpdateGraph(graph,
                configuration.getScanMode(),
                configuration.isCollapseRevisions(),
                configuration.isDisplayingOnlyImpactingRevisions());
    }


    /**
     * @param graph SVNRevisionGraph object
     * @param scanMode if null, revision history, retrieved earlier will be used
     * @param collapseRevisions whether graph should be displayed in 'collapsed revisions' mode 
     * @param displayOnlyImpactingRevisions whether only impacting revisions should be displayed in graph
     */
    private static void createOrUpdateGraph(@NotNull final SVNRevisionGraph graph, 
                                            @Nullable final ScanMode scanMode,
                                            final boolean collapseRevisions, 
                                            final boolean displayOnlyImpactingRevisions) {
        graph.refreshInProgress = true;
        final Project project = graph._project;
        final FilePath filePath = graph._filePath;
        new Task.Backgroundable(project, "Revision History", true, PerformInBackgroundOption.DEAF){
            private FileRevisionHistory history;
            private RevisionGraphModel graphModel;
            private Exception myException;

            @Override
            public void run(@NotNull ProgressIndicator indicator) {
                try {
                    SvnVcs svn = SvnVcs.getInstance(project);
                    if (scanMode != null) {
                        indicator.setText("Collecting revision history for file: " + filePath.getName());
                        history = new FileHistoryRetriever(svn, filePath.getVirtualFile())
                                .getFileHistory(scanMode, indicator);
                    } else {
                        history = graph.revisionHistory;
                    }
                    indicator.setText("Creating graph");
                    indicator.setText2(null);
                    graphModel = new RevisionGraphModel(history, collapseRevisions, displayOnlyImpactingRevisions);
                } catch (Exception e) {
                    myException = e;
                }
            }

            @Override
            public void onSuccess() {
                if (myException == null) {
                    graph.revisionHistory = history;
                    graph.collapseRevisions = collapseRevisions;
                    graph.displayOnlyImpactingRevisions = displayOnlyImpactingRevisions;
                    graph.setModel(graphModel);
                    if (graph.gc == null) {
                        graph.initComponents();
                        graph.show();
                    }
                    // We delay selecting current revision and scrolling to it,
                    // as validation of scroll pane caused by setting new model
                    // is also delayed. If we'll do scrolling before validation,
                    // we can get to wrong place
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            graph.selectAndShowRevision(graphModel.getCurrentRevision());
                        }
                    });

                    SVNRevisionGraphConfiguration configuration = SVNRevisionGraphConfiguration.getInstance(project);
                    configuration.setCollapseRevisions(collapseRevisions);
                    configuration.setDisplayOnlyImpactingRevisions(displayOnlyImpactingRevisions);
                    if (scanMode != null) {
                        graph.scanMode = scanMode;
                        configuration.setScanMode(scanMode);
                        if (history.getStatus().mergeInfoUnavailable
                                && !configuration.isMergeInfoUnavailableWarningSuppressed()) {
                            Notifications.Bus.notify(new Notification("SvnRevisionGraph",
                                    "Merge information is not available",
                                    "Repository uses SVN version 1.4 or earlier. Displayed graph may contain less information.",
                                    NotificationType.WARNING), project);
                            configuration.suppressMergeInfoUnavailableWarning();
                        }
                        if (history.getStatus().retrievedWithErrors) {
                            Notifications.Bus.notify(new Notification("SvnRevisionGraph",
                                    "Error collecting revision information",
                                    "Displayed graph may be incorrect or incomplete. More details are in IDEA's log.",
                                    NotificationType.ERROR), project);
                        }
                    }
                } else {
                    log.error("Error retrieving file history", myException);
                    Messages.showErrorDialog(project, "Error retrieving file history", "Revision Graph Error");
                }
                graph.refreshInProgress = false;
            }

            @Override
            public void onCancel() {
                graph.refreshInProgress = false;
            }
        }.queue();
    }

    private final KeyboardNavigator keyboardNavigator = new KeyboardNavigator(this);
    private FilePath _filePath = null;
    private Project _project = null;
    private FileRevisionHistory revisionHistory;
    private RevisionGraphModel model;
    private GraphComponent gc;
    private final BranchHighlighter branchHighlighter = new BranchHighlighter(this);
    private JTextField _revisionTF = null;
    private JComboBox revisionCB;
    private JPanel revisionPanel;
    private CardLayout revisionDisplay;
    private JTextField _authorTF = null;
    private JTextField _dateTF = null;
    private JTextArea _messageTA = null;
    private ActionPopupMenu viewRevisionPopup;
    private ScanMode scanMode;
    private boolean collapseRevisions;
    private boolean displayOnlyImpactingRevisions;
    private boolean refreshInProgress;

    public SVNRevisionGraph(Project project, FilePath filePath) {
        super(project, "SvnRevisionGraphPlugin.dimensions");
        _filePath = filePath;
        _project = project;
        setTitle("Revision Graph: " + filePath.getPath());
        try {
            setImage(ImageIO.read(getClass().getResource("/icons/svngraph.png")));
        } catch (IOException e) {
            log.error("Error loading icon", e);
        }
        setProject(project);
    }

    private void setModel(RevisionGraphModel graph) {
        model = graph;
        if (gc != null) {
            gc.setGraph(model);
            model.repaint();
        }
        model.getSelectionModel().addListener(mxEvent.CHANGE, this);
    }

    public RevisionGraphModel getModel() {
        return model;
    }

    // should be called in EDT
    public void initComponents() {
        gc = new GraphComponent(model);
        createComponents();
        addMouseHandlers();
        addKeyboardHandler();
        createPopupActions();
    }

    private void createComponents() {
        JPanel p = new JPanel(new BorderLayout());
        p.add(createNorthPanel(), BorderLayout.NORTH);
        p.add(createCenterPanel(), BorderLayout.CENTER);
        p.add(createSouthPanel(), BorderLayout.SOUTH);
        setComponent(p);
        setPreferredFocusedComponent(gc.getGraphControl());
    }

    private JComponent createNorthPanel() {
        DefaultActionGroup toolBarActions = new DefaultActionGroup(
                new DisplayOnlyImpactingRevisionsAction(this),
                new CollapseConsecutiveRevisionsAction(this),
                new Separator(),
                new CompareAction(this),
                new MyGetVersionAction(),
                new ShowPropertiesAction(),
                new ShowPropertiesDiffAction(),
                ShowAllAffectedGenericAction.getInstance(),
                new Separator(),
                new RefreshAction(this));
        for (ScanMode scanMode : ScanMode.values()) {
            toolBarActions.addAction(new ScanModeAction(this, scanMode)).setAsSecondary(true);
        }
        ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.CHANGES_VIEW_TOOLBAR,
                toolBarActions, true);
        // without this toolbar will be validated (and sized) only after 500 ms
        // so focusing on current revision might not work properly
        actionToolbar.updateActionsImmediately();
        return actionToolbar.getComponent();
    }

    private void createPopupActions() {
        ActionManager am = ActionManager.getInstance();

        DefaultActionGroup popupActions = new DefaultActionGroup(
                am.getAction(IdeActions.ACTION_EDIT_SOURCE),
                new CompareAction(this),
                new MyGetVersionAction(),
                new ShowPropertiesAction(),
                new ShowPropertiesDiffAction(),
                ShowAllAffectedGenericAction.getInstance());
        viewRevisionPopup = am.createActionPopupMenu(ActionPlaces.CHANGES_VIEW_POPUP, popupActions);
    }

    @Nullable
    protected JComponent createCenterPanel() {
        JBScrollPane scrollPane = new JBScrollPane(gc.getGraphControl());
        scrollPane.getViewport().setBackground(Color.WHITE);
        scrollPane.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, this);
        return ScrollOverlayFactory.addOverlay(scrollPane, new BranchNamesOverlay(), false, true);
    }

    @Nullable
    protected JComponent createSouthPanel() {
        GridBagLayout gbl = new GridBagLayout();
        JPanel retVal = new JPanel(gbl);
        retVal.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        GridBagConstraints gbc = new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.EAST,
                GridBagConstraints.NONE, new Insets(5, 5, 0, 0), 0, 0);
        retVal.add(new JLabel("Revision:"), gbc);
        gbc.gridy = 1;
        retVal.add(new JLabel("Author:"), gbc);
        gbc.gridy = 2;
        retVal.add(new JLabel("Date:"), gbc);
        gbc.gridx = 1;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.CENTER;
        _revisionTF = new JTextField(TEXTFIELD_WIDTH_IN_CHARS);
        _revisionTF.setMinimumSize(_revisionTF.getPreferredSize());
        _revisionTF.setEditable(false);
        _revisionTF.setBackground(Color.WHITE);
        revisionCB = new JComboBox();
        revisionCB.setBackground(Color.WHITE);
        revisionCB.addItemListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() == ItemEvent.SELECTED) {
                    RevisionInGroup item = (RevisionInGroup) e.getItem();
                    showRevisionProperties(item == null ? null : item.revision);
                }
            }
        });
        revisionDisplay = new CardLayout();
        revisionPanel = new JPanel(revisionDisplay);
        revisionPanel.add(_revisionTF, REVISION_NUMBER_FIELD);
        revisionPanel.add(revisionCB, REVISION_NUMBER_COMBOBOX);
        retVal.add(revisionPanel, gbc);
        gbc.gridy = 1;
        _authorTF = new JTextField(TEXTFIELD_WIDTH_IN_CHARS);
        _authorTF.setMinimumSize(_authorTF.getPreferredSize());
        _authorTF.setEditable(false);
        _authorTF.setBackground(Color.WHITE);
        retVal.add(_authorTF, gbc);
        gbc.gridy = 2;
        _dateTF = new JTextField(TEXTFIELD_WIDTH_IN_CHARS);
        _dateTF.setMinimumSize(_dateTF.getPreferredSize());
        _dateTF.setEditable(false);
        _dateTF.setBackground(Color.WHITE);
        retVal.add(_dateTF, gbc);
        gbc.gridx = 2;
        gbc.gridy = 0;
        gbc.gridheight = 3;
        gbc.anchor = GridBagConstraints.NORTHEAST;
        retVal.add(new JLabel("Message:"), gbc);
        gbc.gridx = 3;
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weightx = 1;
        _messageTA = new JTextArea();
        _messageTA.setEditable(false);
        JScrollPane sp = new JBScrollPane(_messageTA);
        sp.setPreferredSize(new Dimension(300, 0));
        retVal.add(sp, gbc);
        return retVal;
    }

    public void selectAndShowRevision(Revision rev) {
        mxCell cell = model.getCellForRevision(rev);
        model.setSelectionCell(cell);
        gc.scrollCellToVisible(cell);
    }

    private void addMouseHandlers() {
        mxGraphComponent.mxGraphControl graphControl = gc.getGraphControl();
        graphControl.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                maybeShowPopup(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (gc.getGraphControl().isEnabled() && !e.isConsumed() && e.getClickCount() == 2) {
                    mxCell cell = (mxCell) gc.getCellAt(e.getX(), e.getY(), false);
                    if (!revisionHistory.isDirectory() && cell != null && cell.isEdge()) {
                        mxCell cell1 = (mxCell) cell.getSource();
                        mxCell cell2 = (mxCell) cell.getTarget();
                        List<Revision> revs1 = ((RevisionGraphModel.Node) cell1.getValue()).revisions;
                        List<Revision> revs2 = ((RevisionGraphModel.Node) cell2.getValue()).revisions;
                        Revision r1 = revs1.get(revs1.size() - 1);
                        Revision r2 = revs2.get(revs2.size() - 1);
                        if (!r1.isDeleted() && !r2.isDeleted()) {
                            compareFileRevisions(r1, r2);
                        }
                    }
                }
                maybeShowPopup(e);
            }

            private void maybeShowPopup(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    mxCell cell = (mxCell) gc.getCellAt(e.getX(), e.getY());
                    if (cell != null && cell.isVertex()) {
                        viewRevisionPopup.getComponent().show(e.getComponent(), e.getX(), e.getY());
                    }
                }
            }
        });
        RevisionHighlighter revisionHighlighter = new RevisionHighlighter(gc);
        graphControl.addMouseListener(revisionHighlighter);
        graphControl.addMouseMotionListener(revisionHighlighter);
        graphControl.addMouseListener(branchHighlighter);
        graphControl.addMouseMotionListener(branchHighlighter);
    }

    private void addKeyboardHandler() {
        gc.getGraphControl().addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (keyboardNavigator.processEvent(e)) {
                    e.consume();
                }
            }
        });
    }

    @Override
    public void dispose() {
        model = null;
        gc = null;
        _project = null;
        _filePath = null;
        revisionHistory = null;
        super.dispose();
    }

    public void compareFileRevisions(Revision rev1, Revision rev2) {
        if (new Revision.RevisionNumberComparator().compare(rev1, rev2) > 0) {
            Revision tempRev = rev2;
            rev2 = rev1;
            rev1 = tempRev;
        }
        try {
            VcsFileRevision r1 = createVcsFileRevision(rev1, true);
            VcsFileRevision r2 = createVcsFileRevision(rev2, true);
            VcsHistoryUtil.showDiff(_project, _filePath, r1, r2,
                    rev1 + ((model.getCurrentRevision() == rev1) ? "(Local)" : ""),
                    rev2 + ((model.getCurrentRevision() == rev2) ? "(Local)" : ""));
        } catch (Exception e) {
            log.error("Error displaying diff", e);
            Messages.showErrorDialog(_project, "Error showing diff", "Error");
        }
    }

    private String getPathDisplayName(String path, String sourcePath) {
        String[] pathSplit = FileNameUtil.splitPath(path);
        String[] sourceSplit = FileNameUtil.splitPath(sourcePath);
        if (pathSplit != null && sourceSplit != null
                && pathSplit[0].equals(sourceSplit[0])
                && pathSplit[2].equals(sourceSplit[2])) {
            String innerPart = pathSplit[1];
            if (innerPart.equals("/trunk")) {
                return "trunk";
            } else if (innerPart.startsWith("/branches/")) {
                return innerPart.substring(10);
            } else if (innerPart.startsWith("/tags/")) {
                return "Tag: " + innerPart.substring(6);
            }
        }
        return path;
    }

    @Override
    public void invoke(Object sender, mxEventObject evt) {
        selectionChanged();
    }

    public void selectionChanged() {
        List<RevisionGraphModel.Node> selectedNodes = (model == null) ? null : model.getSelectedNodes();
        if (selectedNodes != null && selectedNodes.size() == 1) {
            List<Revision> selectedRevisions = selectedNodes.get(0).revisions;
            if (selectedRevisions.size() == 1) {
                Revision rev = selectedRevisions.get(0);
                _revisionTF.setText("" + rev.getRevisionNumber());
                revisionDisplay.show(revisionPanel, REVISION_NUMBER_FIELD);
                showRevisionProperties(rev);
            } else {
                revisionCB.removeAllItems();
                int index = 1;
                for (Revision r : selectedRevisions) {
                    revisionCB.addItem(new RevisionInGroup(r, index++, selectedRevisions.size()));
                }
                revisionDisplay.show(revisionPanel, REVISION_NUMBER_COMBOBOX);
            }
        } else {
            _revisionTF.setText(null);
            showRevisionProperties(null);
            revisionDisplay.show(revisionPanel, REVISION_NUMBER_FIELD);
        }
    }

    public void moveUpRange() {
        if (revisionCB.isVisible()) {
            int i = revisionCB.getSelectedIndex();
            if (i > 0) {
                revisionCB.setSelectedIndex(i - 1);
            }
        }
    }

    public void moveDownRange() {
        if (revisionCB.isVisible()) {
            int i = revisionCB.getSelectedIndex();
            if (i < revisionCB.getItemCount() - 1) {
                revisionCB.setSelectedIndex(i + 1);
            }
        }
    }

    private void showRevisionProperties(@Nullable Revision rev) {
        if (rev == null) {
            _authorTF.setText(null);
            _dateTF.setText(null);
            _messageTA.setText(null);
        } else {
            _authorTF.setText(rev.getAuthor());
            Date d = rev.getDate();
            if (d != null) {
                _dateTF.setText(DATE_TIME_FORMAT.format(d));
            }
            _messageTA.setText(rev.getMessage());
            _messageTA.setCaretPosition(0); // to prevent scrolling to the end of message
        }
    }

    public List<Revision> getSelectedRevisions() {
        List<Revision> revs = new ArrayList<Revision>();
        if (model != null) {
            List<RevisionGraphModel.Node> selectedNodes = model.getSelectedNodes();
            if (selectedNodes.size() == 1 && revisionCB.isVisible()) {
                revs.add(((RevisionInGroup) revisionCB.getSelectedItem()).revision);
            } else {
                for (RevisionGraphModel.Node node : selectedNodes) {
                    revs.addAll(node.revisions);
                }
            }
        }
        return revs;
    }

    @Override
    public Object getData(@NonNls String dataId) {
        List<Revision> selectedRevisions = getSelectedRevisions();
        Revision selectedRevision = (selectedRevisions.size() == 1) ? selectedRevisions.get(0) : null;
        if (PlatformDataKeys.NAVIGATABLE.is(dataId)) {
            if (selectedRevision != null && !selectedRevision.isDeleted()) {
                VirtualFile vf = createVirtualFileForRevision(selectedRevision);
                return new OpenFileDescriptor(_project, vf);
            }
        } else if (PlatformDataKeys.PROJECT.is(dataId)) {
            return _project;
        } else if (VcsDataKeys.VCS.is(dataId)) {
            return SvnVcs.getInstance(_project).getKeyInstanceMethod();
        } else if (VcsDataKeys.VCS_VIRTUAL_FILE.is(dataId)) {
            if (selectedRevision != null) {
                return new VcsVirtualFile(_filePath.getPath(),
                        createVcsFileRevision(selectedRevision, false),
                        VcsFileSystem.getInstance());
            }
        } else if (VcsDataKeys.VCS_FILE_REVISION.is(dataId)) {
            if (selectedRevision != null) {
                return createVcsFileRevision(selectedRevision, false);
            }
        } else if (CURRENT_REVISION.is(dataId)) {
            return model.getCurrentRevision();
        } else if (SELECTED_REVISION.is(dataId)) {
            return selectedRevision;
        } else if (SELECTED_REVISIONS.is(dataId)) {
            return selectedRevisions;
        } else if (SRC_FILE.is(dataId)) {
            return _filePath;
        }
        return null;
    }

    private VirtualFile createVirtualFileForRevision(Revision r) {
        ContentRevision cr = SvnRepositoryContentRevision.create(SvnVcs.getInstance(_project),
                revisionHistory.getRepoRoot(), r.getRelPath(), null, r.getRevisionNumber());
        return ContentRevisionVirtualFile.create(cr);
    }

    private VcsFileRevision createVcsFileRevision(Revision r, boolean checkCurrentRevision) {
        SVNRevision svnRev = SVNRevision.create(r.getRevisionNumber());
        if (checkCurrentRevision && r == model.getCurrentRevision()) {
            return new CurrentRevision(_filePath.getVirtualFile(), new SvnRevisionNumber(svnRev));
        } else {
            SvnVcs svnVcs = SvnVcs.getInstance(_project);
            return new SvnFileRevision(svnVcs, svnRev, svnRev,
                    revisionHistory.getRepoRoot() + r.getRelPath(), null, null, null, null, null);
        }
    }

    public void refresh() {
        createOrUpdateGraph(this, scanMode, collapseRevisions, displayOnlyImpactingRevisions);
    }

    public boolean isRefreshInProgress() {
        return refreshInProgress;
    }

    public ScanMode getScanMode() {
        return scanMode;
    }

    public void setScanMode(ScanMode scanMode) {
        if (this.scanMode != scanMode) {
            createOrUpdateGraph(this, scanMode, collapseRevisions, displayOnlyImpactingRevisions);
        }
    }

    public boolean isCollapseRevisions() {
        return collapseRevisions;
    }

    public void setCollapseRevisions(boolean collapseRevisions) {
        if (collapseRevisions != this.collapseRevisions) {
            createOrUpdateGraph(this, null, collapseRevisions, displayOnlyImpactingRevisions);
        }
    }

    public boolean isDisplayingOnlyImpactingRevisions() {
        return displayOnlyImpactingRevisions;
    }

    public void setDisplayOnlyImpactingRevisions(boolean displayOnlyImpactingRevisions) {
        if (displayOnlyImpactingRevisions != this.displayOnlyImpactingRevisions) {
            createOrUpdateGraph(this, null, collapseRevisions, displayOnlyImpactingRevisions);
        }
    }

    public int getBranchForY(int y) {
        if (model == null) {
            return -1;
        } else {
            int stripeHeight = model.getNodeHeight() + SVNRevisionGraph.CELL_SPACING;
            return y < 0 ? -1 : ((y - 7) / stripeHeight);
        }
    }

    public void repaintBranch(int n) {
        if (model != null) {
            JComponent graphControl = gc.getGraphControl();
            int step = model.getNodeHeight() + CELL_SPACING;
            graphControl.repaint(0, CELL_SPACING - 14 + n * step, graphControl.getWidth(), 13);
        }
    }
    
    private class BranchNamesOverlay extends JComponent {
        @Override
        public void paint(Graphics g) {
            if (model != null) {
                int step = model.getNodeHeight() + CELL_SPACING;
                int currentY = CELL_SPACING - 4;
                FontMetrics fm = g.getFontMetrics();
                int i = 0;
                int highlightedRow = branchHighlighter.getHighlightedRow();
                for (String path : model.getAllPaths()) {
                    boolean highlighted = (i++ == highlightedRow);
                    String displayedPath = getPathDisplayName(path, revisionHistory.getRelPath());
                    int lineWidth = fm.stringWidth(" " + displayedPath + " ");
                    g.setColor(highlighted ? BRANCH_LABEL_HIGHLIGHTED_COLOR : BRANCH_LABEL_COLOR);
                    g.fillRoundRect(0, currentY - 10, lineWidth, 12, 7, 7);
                    g.setColor(highlighted ? BRANCH_LABEL_BORDER_HIGHLIGHTED_COLOR : BRANCH_LABEL_BORDER_COLOR);
                    g.drawRoundRect(0, currentY - 10, lineWidth, 12, 7, 7);
                    g.setColor(BRANCH_NAMES_COLOR);
                    g.drawString(displayedPath, 4, currentY);
                    currentY += step;
                }
            }
        }
    }

    private static class RevisionShape extends mxBasicShape {
        @Override
        public Color getFillColor(mxGraphics2DCanvas canvas, mxCellState state) {
            switch (RevisionHighlighter.getState((mxCell) state.getCell())) {
                case OFF:
                    return state.getView().getGraph().isCellSelected(state.getCell()) ? SELECTED_REVISION_COLOR : REVISION_COLOR;
                case PARTIAL:
                    return PARTIALLY_HIGHLIGHTED_REVISION_COLOR;
                case ON:
                    return HIGHLIGHTED_REVISION_COLOR;
                default:
                    return null;
            }
        }

        @Override
        public void paintShape(mxGraphics2DCanvas canvas, mxCellState state) {
            Rectangle rect = state.getRectangle();
            Rectangle innerRect = new Rectangle(rect.x + 1, rect.y + 1, rect.width - 1, rect.height - 1);

            // Paints the background
            if (configureGraphics(canvas, state, true))
            {
                canvas.fillShape(innerRect, hasShadow(canvas, state));
            }

            // Paints the foreground
            if (configureGraphics(canvas, state, false))
            {
                canvas.getGraphics().drawRect(rect.x, rect.y, rect.width,
                        rect.height);
            }
        }
    }

    private static class LinkShape extends mxConnectorShape {
        @Override
        public void paintShape(mxGraphics2DCanvas canvas, mxCellState state) {
            // mxConnectorShape doesn't properly use getStrokeColor, so we cannot just override it to change color
            Object savedColor = state.getStyle().get(mxConstants.STYLE_STROKECOLOR);
            try {
                String color = RevisionHighlighter.getState((mxCell) state.getCell()) == RevisionHighlighter.State.OFF ? "black" : "red";
                state.getStyle().put(mxConstants.STYLE_STROKECOLOR, color);
                super.paintShape(canvas, state);
            } finally {
                state.getStyle().put(mxConstants.STYLE_STROKECOLOR, savedColor);
            }
        }
    }
    
    private static class RevisionInGroup {
        private final Revision revision;
        private final int index;
        private final int groupSize;

        private RevisionInGroup(Revision revision, int index, int groupSize) {
            this.revision = revision;
            this.index = index;
            this.groupSize = groupSize;
        }

        @Override
        public String toString() {
            return Long.toString(revision.getRevisionNumber()) + " (" + index + " of " + groupSize + ")";
        }
    }

    private class GraphComponent extends mxGraphComponent {
        public GraphComponent(mxGraph graph) {
            super(graph);
            getConnectionHandler().setEnabled(false);
            getGraphHandler().setMoveEnabled(false);
            getGraphHandler().setMarkerEnabled(false);
            getGraphHandler().setVisible(false);
            getSelectionCellsHandler().setMaxHandlers(0);
            setDragEnabled(false);
        }

        @Override
        protected mxSelectionCellsHandler createSelectionCellsHandler() {
            return new mxSelectionCellsHandler(this) {
                @Override
                public void paintHandles(Graphics g) {
                }
            };
        }

        @Override
        // this is required as we use our own scroll pane, and not the one provided by JGraphX (mxGraphComponent)
        protected void installFocusHandler() {
            graphControl.addMouseListener(new MouseAdapter() {
                public void mousePressed(MouseEvent e) {
                    if (!graphControl.hasFocus()) {
                        graphControl.requestFocus();
                    }
                }
            });
        }

        @Override
        // this is required as we use our own scroll pane, and not the one provided by JGraphX (mxGraphComponent)
        public JViewport getViewport() {
            Container parent = (graphControl == null) ? null : graphControl.getParent();
            return (parent instanceof JViewport) ? (JViewport) parent : super.getViewport();
        }

        @Override
        // Original implementation doesn't recreate selection handler,
        // it continues to refer to the previous mxGraph instance.
        // I believe this is a bug.
        public void setGraph(mxGraph value) {
            super.setGraph(value);
            selectionCellsHandler = createSelectionCellsHandler();
        }

        @Override
        protected void paintBackground(Graphics g) {
            if (model != null) {
                int width = graphControl.getWidth();
                int height = graphControl.getHeight();
                Graphics2D g2 = (Graphics2D) g;
                g2.setColor(BRANCH_SEPARATING_LINE_COLOR);
                g2.setStroke(BRANCH_SEPARATING_LINE_STROKE);
                int stripeHeight = model.getNodeHeight() + CELL_SPACING;
                int n = model.getAllPaths().size();
                Rectangle clip = g2.getClipBounds();
                for (int y = 7, i = 0; y < height && i <= n; y += stripeHeight, i++) {
                    if (clip == null) {
                        g2.drawLine(0, y, width, y);
                    } else if (y >= clip.y && y <= (clip.y + clip.height)) {
                        g2.drawLine(clip.x, y, clip.x + clip.width, y);
                    }
                }
            }
        }
    }
}
